package cloudspace.vm;

import java.io.File;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


import javassist.ClassPool;
import javassist.Loader;
import javassist.NotFoundException;

import org.apache.log4j.Logger;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Session;
import org.zkoss.zk.ui.Sessions;

import bsh.Interpreter;
import cloudspace.vm.filesystem.ProjectSpec;
import cloudspace.vm.io.console.InfoStream;
import cloudspace.vm.io.console.SystemStreamMultiplexer;
import cloudspace.vm.javassist.JavassistBootloader;
import cloudspace.vm.javassist.MalformedConfigurationException;

//TODO: implement a strategy for handling memory management
/**
 * The Class VM. This is a basic Cloudspace Virtual Machine. This virtual
 * Machine provides shared information about the specific virtual machine to
 * every virtual machine context created in this context. This virtual machine
 * holds information about. local classes, local loader, delegated classes,
 * standard output streams, and parent loaders. There is one of these per
 * instance of a virtual machine.
 */
public class VM
{
    private Map<String, Object> localObjects = new HashMap<String,Object>();
    /**
     * An exception that occurs when a thread requests a VM and there is no vm
     * tagged in the threadLocal.
     *
     * @author mjw87
     *
     */
    public static class NoVmException extends RuntimeException
    {

        /**
         *
         */
        private static final long serialVersionUID = 524965398230936313L;

        public NoVmException(String msg)
        {
            super(msg);
        }

    }

    protected static Logger logger = Logger.getLogger(VM.class);

    // -------------------------------------------------------------------------
    /**
     * The Class LoggedClassPool. This class represents a Logged class pool that
     * can detect updates to the class files stored internally.
     */
    private class LoggedClassPool extends ClassPool
    {
        List<File> currentClassPath;
        /** The logged class path entries. */
        private long timeStamp;

        public LoggedClassPool(ClassPool parent)
        {
            super(parent);
            init();
        }

        /**
         * Initializes the class pool's variables.
         */
        private void init()
        {
            this.logUpdate();
        }

        /**
         * This private method recurses through the classpath entries to log
         * time stamps for every file in the class path.
         *
         * @param classPathDir
         *            the class path directory to recurse and log.
         */
        private List<File> recurseClassPathEntry(File root)
        {
            List<File> flattenList = new ArrayList<File>();
            File[] dirList = root.listFiles();
            if (dirList != null)
            {
                for (File classPathEntry : dirList)
                {
                    if (classPathEntry.isDirectory())
                    {
                        flattenList.addAll(recurseClassPathEntry(classPathEntry));
                    }
                    else
                    {
                        flattenList.add(classPathEntry);
                    }
                }
            }
            return flattenList;
        }

        /**
         * Checks if the class pool has been updated
         *
         * @return true, if is updated
         */
        public boolean isUpdated()
        {
            for (File oldFiles : currentClassPath)
            {
                if (!oldFiles.exists())
                    return true;
            }
            List<File> flatList = recurseClassPathEntry(localRoot);
            for (File lFile : flatList)
            {
                if (lFile.lastModified() > timeStamp)
                {
                    return true;
                }
            }
            return false;
        }

        public void logUpdate()
        {
            timeStamp = java.util.Calendar.getInstance().getTime().getTime();
            currentClassPath = this.recurseClassPathEntry(localRoot);
        }
    }

    /**
     * This set keeps track of all of the desktops associated with this virtual
     * machine.
     */
    //private Set<String> desktops;
    /**
     * The local root directory for the virtual machine.
     */
    private File localRoot;

    /**
     * The Enum PrinterType.
     */
    public enum PrinterType
    {

        /** The OUT. */
        OUT,
        /** The ERROR. */
        ERROR
    };

    private String loaderStamp;
    /**
     * The protected Info Stream tags for VM provided streams.
     */
    public static final String standardOutTag = "Standard Out";
    public static final String standardErrorTag = "Standard Error";
    public static final String logTag = "CloudSpace Logs";
    /**
     * The Standard Out stream and Printer. The printer is pre wrapped just for
     * convenience this allows a user to perform VM.out to print to the VM
     * specfic standard out
     */
    private final InfoStream stdOut = new InfoStream(standardOutTag);
    public final PrintStream out = new PrintStream(stdOut);
    /** See above for reasoning */
    private final InfoStream stdErr = new InfoStream(standardErrorTag);
    public final PrintStream err = new PrintStream(stdErr);
    private final InfoStream stdLog = new InfoStream(logTag);
    public final PrintStream log = new PrintStream(stdLog);

    private Lock eventLock = new ReentrantLock();
    
    /** The mapping from local Directories (unique to each VM) to cloudspaceVMs. */
    private static Map<String, VM> directoryToVM = new HashMap<String, VM>();

    /**
     * The current loader for the VM. This is updated when classes have been
     * changed in the classpool.
     */
    private Loader vmLoader;
    private LoggedClassPool vmPool;

    /** The thread current vm. */
    private static ThreadLocal<VM> threadCurrentVM = new ThreadLocal<VM>();
    private static ThreadLocal<ClassLoader> threadOldLoader = new ThreadLocal<ClassLoader>();

    /**
     * Enter this VM and associated current desktop with it.
     */
    public void lockVM()
    {
    	eventLock.lock();
    }
    /**
     * This silently sets the currentVM  it will not take ownership of the VM!
     * BECAREFUL!!
     * 
     */
	public void enter()
	{
		enter0();
		
	}
    /**
     * Enter this VM and associate desktop with id 'dID' with the VM.
     */
    private void enter0()
    {
		
		threadCurrentVM.set(this);
		threadOldLoader.set(Thread.currentThread().getContextClassLoader());
		Thread.currentThread().setContextClassLoader(this.getLoader());
		//desktopID.set(dID);
    }

    /**
     * Leave this VM.
     */
    public void unlockVM()
    {
    	eventLock.unlock();
    }
    /**
     * This silently leaves the VM's context.
     * BECAREFUL!
     * 
     */
    public static void leave()
    {
    	leave0();
    }
    private static void leave0()
    {
    	threadCurrentVM.set(null);
    	ClassLoader loader = threadOldLoader.get();
    	threadOldLoader.set(null);
    	Thread.currentThread().setContextClassLoader(loader);
    }

    /**
     * Gets the current VM.
     *
     * @return the current VM
     */
    public static VM currentVM()
    {
        try
        {
            return threadCurrentVM.get();
        }
        catch (NullPointerException e)
        {
            throw new NoVmException("There is no VM associated with this thread");
        }
    }

    /**
     * This method looks up the current VM context a class is acting in. It uses
     * ZK desktop and page information to determine which vm instance a thread
     * is acting in.
     *
     * Will not enter VM.
     *
     * @return the instance of the vm associated with the current desktop's
     *         basedir
     */
    public static VM lookupVM()
    {
        String currentPage = Executions.getCurrent().getDesktop().getRequestPath();
        String currentDir = getBaseDir(currentPage);
        String localRoot = Executions.getCurrent().getDesktop().getWebApp().getRealPath(currentDir);
        return lookupVM(localRoot);
    }

    /**
     * Look up VM associated with 'baseDir', creating it if necessary.
     */
    public static VM lookupVM(String baseDir)
    {
    	String projectBaseDir = ProjectSpec.getBaseProjectDir(baseDir);
        VM vm = directoryToVM.get(projectBaseDir);
        logger.info("Looking up "+baseDir+" as "+projectBaseDir);
        if (vm == null)
        {
            vm = new VM(projectBaseDir);

            vm.addEventListener(new VM.EventListenerAdapter() {

                @Override
                public void onClassReload()
                {
                    /*
                     * We must reset session attributes if classes are reloaded.
                     * Determine if 'app-store' is sufficient or not.
                     */
                    Session session = Sessions.getCurrent();
                    for (Object attrName : session.getAttributes().keySet())
                    {
                        if (((String) attrName).contains("app-store"))
                        {
                        	logger.info("onClassReload: Clearing session attribute: "
                                    + (String) attrName);
//                            System.out.println("onClassReload: Clearing session attribute: "
//                                    + (String) attrName);
                            session.removeAttribute((String) attrName);
                        }
                    }

                }

            });
            directoryToVM.put(projectBaseDir, vm);
        }
        return vm;
    }
public boolean bounceLoader = false;
    /**
     * This private member requests an update to the Class Loader registered to
     * this virtual Machine. The update will only happen if a class in the class
     * pool has been updated.
     */
    private void updateLoader()
    {
        synchronized (vmPool)
        {
            if (vmPool.isUpdated() || bounceLoader)
            {
                logger.info("Updating classes.");
                updateLoader0();
                bounceLoader = false;

            }
        }

    }

    /**
     * Gets the base dir of a page. This method assumes a
     * www.webpage.domain/<UNIQUE PAGE TAG>/...... If the format differs from
     * this, cloudspace will not work.
     *
     * @param currentPage
     *            string representing the current request path.
     *
     * @return the base dir representation of a page request.
     */
    private static String getBaseDir(String currentPage)
    {
        int index = currentPage.indexOf('/');
        int index2 = currentPage.lastIndexOf("/");
        String callPath = currentPage.substring(index, index2);
        return callPath;
    }

    /**
     * Instantiates a new cloudspace vm. Using a localRoot and desktop ID.
     *
     * @param vmContext
     *            the desk id
     * @param localRoot
     *            the local root
     */
    public VM(String projectBaseDir)
    {
        //desktops = new HashSet<String>();
        localRoot = new File(projectBaseDir);
        logger.info("Creating new Virtual Machine for "+localRoot.getPath()+" on request path "+projectBaseDir);
        updateLoader0();
    }
    public void updateLoader0()
    {
        this.log.println("Source code has changed, server has been updated!");
        final ClassLoader parentLoader = this.getClass().getClassLoader();
        ClassPool proxyPool = JavassistBootloader.getProxyPool();
        vmPool = new LoggedClassPool(proxyPool);

        /* define locations in which rewritable .class files are */
        try
        {
            if (Executions.getCurrent() != null)
            {
                logger.info("("+localRoot.getPath()+") Adding /WEB-INF/pagelib/ to classpath");
                vmPool.appendClassPath(Executions.getCurrent().getDesktop().getWebApp()
                        .getRealPath("/WEB-INF/pagelib/")+"*");
            }
            logger.info("("+localRoot.getPath()+") Adding local directory to classpath");
            vmPool.appendClassPath(localRoot.getAbsolutePath());
            logger.info("("+localRoot.getPath()+") Adding local directory jars to classpath");
            vmPool.appendClassPath(localRoot + File.separator + "*");
            logger.info("("+localRoot.getPath()+") Adding classes directory within the local directory to classpath");
            vmPool.appendClassPath(localRoot + File.separator + "classes" + File.separator + "*");
            logger.info("("+localRoot.getPath()+") Adding System classpath to classpath");
            vmPool.appendSystemPath();
        }
        catch (NotFoundException e)
        {
            logger.error("Class pool could not initialize because of missing directories",e);
        }
        vmPool.logUpdate();

        /* always attempt to delegate to parent first */
        vmLoader = new Loader(parentLoader, vmPool) {
            protected Class<?> loadClassByDelegation(String clazzName) throws ClassNotFoundException
            {
                if (clazzName.startsWith("_test."))
                {
                    return null;
                }
                try
                {
                    return delegateToParent(clazzName);
                }
                catch (ClassNotFoundException ncdef)
                {
                    return null;
                }
            }
        };
        loaderStamp = Calendar.getInstance().getTime().toString();
        try
        {
            JavassistBootloader.translateClasses(vmLoader, vmPool);
        }
        catch (MalformedConfigurationException e)
        {
            logger.error("Configuration problem in Javassist config",e);
        }
        runLoaderListeners();
    }

    /**
     * Prepares an interpreter for use in the current context.
     *
     * @param ip
     */
    public void prepareInterpreter(Interpreter ip)
    {
        updateLoader();
        ip.setClassLoader(vmLoader);
    }

    /**
     * Gets the Info Stream operating as a backend to System.out.
     *
     * @return InfoStream System.out
     */
    public InfoStream getStdOut()
    {
        return stdOut;
    }

    /**
     * Gets the Info Stream operating as a backend to System.err.
     *
     * @return InfoStream System.err
     */
    public InfoStream getError()
    {
        return stdErr;
    }

    /**
     * This gets a VM hardwired stream according to a printer type. Currently,
     * only system.out and system.err are hardwired.
     *
     * @param streamID
     * @return the protected VM stream corresponding to the passed printer type.
     */
    public PrintStream getStream(PrinterType streamID)
    {
        switch (streamID)
        {
        case OUT:
            return out;
        case ERROR:
            return err;
        default:
            return null;
        }
    }

    /**
     * Get the unique local root for this VM. A VM only has one local root and
     * it does not share this local root with any other VM.
     *
     * @return The root directory for the VM.
     */
    public String getLocalRoot()
    {
        return localRoot.getAbsolutePath();
    }

    /**
     * Gets the class pool for this VM
     *
     * @return the VM's class pool
     */
    public ClassPool getClassPool()
    {
        return vmPool;
    }

    public boolean isLocalClass(final String classname)
    {
        return vmPool.find(classname) != null || classname.startsWith("_test.wrap");
    }

    /**
     * Gets the class loader used for all bean shell interpreters in this VM's
     * Context
     *
     * @return the current Context's class loader
     */
    public Loader getLoader()
    {
        return vmLoader;
    }

    // Static init block. This sets up the SystemStreamMultiplexer to start
    // intercepting system.out and system.err
    static
    {
        if (!(System.out instanceof SystemStreamMultiplexer))
        {
            SystemStreamMultiplexer ssm = new SystemStreamMultiplexer(System.out);
            ssm.setStream(PrinterType.OUT);
            System.setOut(ssm);
        }
        if (!(System.err instanceof SystemStreamMultiplexer))
        {
            SystemStreamMultiplexer ssmErr = new SystemStreamMultiplexer(System.err);
            ssmErr.setStream(PrinterType.ERROR);
            System.setErr(ssmErr);
        }
    }

    /**
     * Gets the Info Stream representing the CloudSpace Log.
     *
     * @return the protected InfoStream Log
     */
    public InfoStream getLog()
    {
        return stdLog;
    }

    /**
     * Get the last time the loader was initialized.
     *
     * @return the current Loader timestamp
     */
    public String getLoaderTime()
    {
        return loaderStamp;
    }

    /**
     * Registers a desktop to the current VM.
     *
     * @param dID
     *            Desktop ID.
     */
    /*public void addDesktop(String dID)
    {
        if(!desktops.contains(dID))
        desktops.add(dID);
    }*/

    /**
     * de registers a Desktop ID from the Virtual Machine.
     *
     * @param dID
     *            the Desktop ID to remove.
     */
/*    public void removeDesktop(String dID)
    {
        desktops.remove(dID);
    }*/

    /**
     * Gets the number desktops registered to this VM.
     *
     * @return the number desktops
     */
/*    public int getNumberDesktops()
    {
        return desktops.size();
    }*/

    /**
     * A class listeners for VM events must implement.
     */
    public interface EventListener
    {
        /**
         * Called if VM reloads user classes.
         */
        public void onClassReload();

        /* Add others here. */
    }

    /**
     * An adapter class for VM.EventListener
     */
    public static abstract class EventListenerAdapter implements EventListener
    {
        @Override
        public void onClassReload()
        {
        }
    }

    private List<EventListener> vmListeners = new ArrayList<EventListener>();

    /**
     * Add an VM event listener.
     */
    public void addEventListener(EventListener listener)
    {
        vmListeners.add(listener);
    }

    /**
     * Remove an VM event listener.
     */
    public void removeEventListener(EventListener listener)
    {
        vmListeners.remove(listener);
    }

    private void runLoaderListeners()
    {
        for (EventListener l : vmListeners)
        {
            l.onClassReload();
        }
    }
    public void setLocalObject(String key, Object obj)
    {
        localObjects.put(key, obj);
    }
    public Object getLocalObject(String key)
    {
        return localObjects.get(key);
    }


}
